diff options
Diffstat (limited to 'app/api/auth/[...nextauth]/route.ts')
| -rw-r--r-- | app/api/auth/[...nextauth]/route.ts | 85 |
1 files changed, 52 insertions, 33 deletions
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 2b168746..e059377c 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,4 +1,3 @@ -// auth/config.ts - 업데이트된 NextAuth 설정 import NextAuth, { NextAuthOptions, Session, @@ -14,16 +13,15 @@ import { verifyOtpTemp } from '@/lib/users/verifyOtp' import { getSecuritySettings } from '@/lib/password-policy/service' import { verifySmsToken } from '@/lib/users/auth/passwordUtil' import { SessionRepository } from '@/lib/users/session/repository' -import { loginSessions } from '@/db/schema' // 인증 방식 타입 정의 type AuthMethod = 'otp' | 'email' | 'sgips' | 'saml' -// 모듈 보강 선언 (기존과 동일) +// 모듈 보강 선언 - ID를 string으로 통일 declare module "next-auth" { interface Session { user: { - id: string + id: string // number → string으로 변경 name?: string | null email?: string | null image?: string | null @@ -33,12 +31,12 @@ declare module "next-auth" { reAuthTime?: number | null authMethod?: AuthMethod sessionExpiredAt?: number | null - dbSessionId?: string | null // DB 세션 ID 추가 + dbSessionId?: string | null } } interface User { - id: string + id: string // number → string으로 변경 imageUrl?: string | null companyId?: number | null techCompanyId?: number | null @@ -51,7 +49,7 @@ declare module "next-auth" { declare module "next-auth/jwt" { interface JWT { - id?: string + id?: string // 이미 string이므로 그대로 imageUrl?: string | null companyId?: number | null techCompanyId?: number | null @@ -63,6 +61,15 @@ declare module "next-auth/jwt" { } } +// 타입 변환 헬퍼 함수들 +function ensureString(value: string | number): string { + return String(value) +} + +function ensureNumber(value: string | number): number { + return typeof value === 'string' ? parseInt(value, 10) : value +} + // 보안 설정 캐시 (기존과 동일) let securitySettingsCache: { data: any | null @@ -71,7 +78,7 @@ let securitySettingsCache: { } = { data: null, lastFetch: 0, - ttl: 5 * 60 * 1000 // 5분 캐시 + ttl: 5 * 60 * 1000 } async function getCachedSecuritySettings() { @@ -85,7 +92,7 @@ async function getCachedSecuritySettings() { } catch (error) { console.error('Failed to fetch security settings:', error) securitySettingsCache.data = { - sessionTimeoutMinutes: 480 // 8시간 기본값 + sessionTimeoutMinutes: 480 } } } @@ -111,7 +118,7 @@ function getClientIP(req: any): string { export const authOptions: NextAuthOptions = { providers: [ - // OTP 로그인 (기존 유지) + // OTP 로그인 - 타입 에러 수정 CredentialsProvider({ id: 'credentials-otp', name: 'OTP', @@ -130,8 +137,9 @@ export const authOptions: NextAuthOptions = { const securitySettings = await getCachedSecuritySettings() const reAuthTime = Date.now() + // 반환 객체의 id를 string으로 변환 return { - id: String(user.id ?? email ?? "dts"), + id: ensureString(user.id), // ✅ string으로 변환 email: user.email, imageUrl: user.imageUrl ?? null, name: user.name, @@ -144,12 +152,12 @@ export const authOptions: NextAuthOptions = { }, }), - // MFA 완료 후 최종 인증 (DB 연동 버전) + // MFA 완료 후 최종 인증 - 타입 에러 수정 CredentialsProvider({ id: 'credentials-mfa', name: 'MFA Verification', credentials: { - userId: { label: 'User ID', type: 'text' }, + userId: { label: 'User ID', type: 'text' }, // number → text로 변경 smsToken: { label: 'SMS Token', type: 'text' }, tempAuthKey: { label: 'Temp Auth Key', type: 'text' }, }, @@ -159,28 +167,29 @@ export const authOptions: NextAuthOptions = { return null } + // userId를 number로 변환하여 DB 조회 + const numericUserId = ensureNumber(credentials.userId) + const user = await getUserById(numericUserId) + if (!user) { + console.error('User not found after MFA verification') + return null + } + try { // DB에서 임시 인증 정보 확인 const tempAuth = await SessionRepository.getTempAuthSession(credentials.tempAuthKey) - if (!tempAuth || tempAuth.userId !== credentials.userId) { + if (!tempAuth || ensureNumber(tempAuth.userId) !== user.id) { console.error('Temp auth expired or not found') return null } // SMS 토큰 검증 - const smsVerificationResult = await verifySmsToken(Number(credentials.userId), credentials.smsToken) + const smsVerificationResult = await verifySmsToken(user.id, credentials.smsToken) if (!smsVerificationResult || !smsVerificationResult.success) { console.error('SMS token verification failed') return null } - // 사용자 정보 조회 - const user = await getUserById(Number(credentials.userId)) - if (!user) { - console.error('User not found after MFA verification') - return null - } - // 임시 인증 정보를 사용됨으로 표시 await SessionRepository.markTempAuthSessionAsUsed(credentials.tempAuthKey) @@ -194,7 +203,7 @@ export const authOptions: NextAuthOptions = { const userAgent = req.headers?.['user-agent'] const dbSession = await SessionRepository.createLoginSession({ - userId: String(user.id), + userId: user.id, // number로 전달 ipAddress, userAgent, authMethod: tempAuth.authMethod, @@ -203,8 +212,9 @@ export const authOptions: NextAuthOptions = { console.log(`MFA completed for user ${user.email} (${tempAuth.authMethod})`) + // 반환 객체의 id를 string으로 변환 return { - id: String(user.id), + id: ensureString(user.id), // ✅ string으로 변환 email: user.email, imageUrl: user.imageUrl ?? null, name: user.name, @@ -257,7 +267,7 @@ export const authOptions: NextAuthOptions = { session: { strategy: 'jwt', - maxAge: 30 * 24 * 60 * 60, // 30일 + maxAge: 30 * 24 * 60 * 60, }, callbacks: { @@ -268,7 +278,7 @@ export const authOptions: NextAuthOptions = { // 최초 로그인 시 (MFA 완료 후) if (user) { const reAuthTime = Date.now() - token.id = user.id + token.id = user.id // ✅ 이제 둘 다 string 타입 token.email = user.email token.name = user.name token.companyId = user.companyId @@ -288,8 +298,8 @@ export const authOptions: NextAuthOptions = { try { const dbSession = await SessionRepository.createLoginSession({ - userId: token.id, - ipAddress: '0.0.0.0', // SAML의 경우 IP 추적 제한적 + userId: ensureNumber(token.id), // string을 number로 변환하여 DB에 저장 + ipAddress: '0.0.0.0', authMethod: 'saml', sessionExpiredAt, }) @@ -346,7 +356,7 @@ export const authOptions: NextAuthOptions = { if (token) { session.user = { - id: token.id as string, + id: token.id as string, // ✅ string으로 일관성 유지 email: token.email as string, name: token.name as string, domain: token.domain as string, @@ -386,14 +396,16 @@ export const authOptions: NextAuthOptions = { // 이미 MFA에서 DB 세션이 생성된 경우가 아니라면 여기서 생성 if (account?.provider !== 'credentials-mfa' && user.id) { try { + const numericUserId = ensureNumber(user.id) // string을 number로 변환 + // 기존 활성 세션 확인 - const existingSession = await SessionRepository.getActiveSessionByUserId(user.id) + const existingSession = await SessionRepository.getActiveSessionByUserId(numericUserId) if (!existingSession) { const sessionExpiredAt = new Date(Date.now() + (securitySettings.sessionTimeoutMinutes * 60 * 1000)) await SessionRepository.createLoginSession({ - userId: user.id, - ipAddress: '0.0.0.0', // signIn 이벤트에서는 IP 접근 제한적 + userId: numericUserId, + ipAddress: '0.0.0.0', authMethod: user.authMethod || 'unknown', sessionExpiredAt, }) @@ -415,8 +427,15 @@ export const authOptions: NextAuthOptions = { await SessionRepository.logoutSession(dbSessionId) } else if (userId) { // dbSessionId가 없는 경우 사용자의 모든 활성 세션 로그아웃 - await SessionRepository.logoutAllUserSessions(userId) + const numericUserId = ensureNumber(userId) // string을 number로 변환 + await SessionRepository.logoutAllUserSessions(numericUserId) } } } } + + +const handler = NextAuth(authOptions) + +// ✅ 핵심: 반드시 GET, POST를 named export로 내보내야 함 +export { handler as GET, handler as POST }
\ No newline at end of file |
